今天我們來了解與時間有關的型別(在Polars中習慣稱呼其為temporal
型別)及操作。
Polars共有四種temporal
型別:
2025-09-01
,內部儲存型態為pl.Int32
,代表自UNIX epoch
(即UTC1970年1月1日0時0分0秒)以來的天數。2025-09-01 07:00:00
,內部儲存型態為pl.Int64
,代表自UNIX epoch
以來的秒數,可以選擇三種秒數單位,分別為us
、ns
及ms
,預設值為us
(microseconds)。timedelta
。本日大綱如下:
pl.DataFrame.group_by_dynamic()
pl.DataFrame.upsample()
codepanda
使用多個生成時間序列的函數,來建立df
dataframe。
以pl.date_range()為例,其共有五個參數:
start=
:起始日期。end=
:結束日期。interval=
:時間間隔。closed=
:是否包括起始與結束日期。共有both
(預設)、left
及right
三種。eager=
:預設為False
,代表不立即生成,而是返回expr。如果設定為True
,則會立即生成,並返回series。from datetime import date, datetime, time, timedelta
import pandas as pd
import polars as pl
df = pl.DataFrame(
{
"date": pl.date_range(
date(2025, 1, 1), date(2025, 6, 1), interval="1mo", eager=True
),
"date_str": [
"2025-07-05",
"2025-08-05",
"2025-09-10",
"2025-10-10",
"2025-11-20",
"2025-12-20",
],
"datetime": pl.datetime_range(
datetime(2025, 1, 1),
datetime(2025, 1, 2),
interval="4h",
closed="left",
eager=True,
),
"datetime_utc": pl.datetime_range(
datetime(2025, 1, 1),
datetime(2025, 6, 1),
interval="1mo",
eager=True,
time_zone="UTC",
),
"time": pl.time_range(
time(13, 0, 0),
time(13, 25, 0),
interval=timedelta(minutes=5),
eager=True,
),
}
)
shape: (6, 5)
┌────────────┬────────────┬─────────────────────┬─────────────────────────┬
│ date ┆ date_str ┆ datetime ┆ datetime_utc ┆
│ --- ┆ --- ┆ --- ┆ --- ┆
│ date ┆ str ┆ datetime[μs] ┆ datetime[μs, UTC] ┆
╞════════════╪════════════╪═════════════════════╪═════════════════════════╪
│ 2025-01-01 ┆ 2025-07-05 ┆ 2025-01-01 00:00:00 ┆ 2025-01-01 00:00:00 UTC ┆
│ 2025-02-01 ┆ 2025-08-05 ┆ 2025-01-01 04:00:00 ┆ 2025-02-01 00:00:00 UTC ┆
│ 2025-03-01 ┆ 2025-09-10 ┆ 2025-01-01 08:00:00 ┆ 2025-03-01 00:00:00 UTC ┆
│ 2025-04-01 ┆ 2025-10-10 ┆ 2025-01-01 12:00:00 ┆ 2025-04-01 00:00:00 UTC ┆
│ 2025-05-01 ┆ 2025-11-20 ┆ 2025-01-01 16:00:00 ┆ 2025-05-01 00:00:00 UTC ┆
│ 2025-06-01 ┆ 2025-12-20 ┆ 2025-01-01 20:00:00 ┆ 2025-06-01 00:00:00 UTC ┆
└────────────┴────────────┴─────────────────────┴─────────────────────────┴
┬──────────┐
┆ time │
┆ --- │
┆ time │
╪══════════╡
┆ 13:00:00 │
┆ 13:05:00 │
┆ 13:10:00 │
┆ 13:15:00 │
┆ 13:20:00 │
┆ 13:25:00 │
┴──────────┘
以下我們將透過幾段程式碼,認識temporal
的各種型別及dt
命名空間提供的各種功能。
pl.Date
轉換為pl.String
下面這段程式碼展示了:
pl.String
型別。temporal
型別中的時間資訊時,需要加上()
,也就是需要使用pl.Expr.dt.month()
。習慣使用Pandas的朋友,常常會寫成pl.Expr.dt.month
。pl.Expr.cast
將「"date"」列轉換為pl.Int32
型別,可以得到其距離UNIX epoch
的天數。pl.Expr.cast
將「"datetime"」列轉換為pl.Int64
型別,可以得到其距離UNIX epoch
的秒數(單位為microseconds)。(
df.select(
pl.col("date"),
pl.col("date").dt.strftime("%Y/%m/%d").alias("strftime"),
pl.col("date").dt.month().alias("month"), # not `dt.month`
pl.col("date").cast(pl.Int32).alias("date_in_days"),
pl.col("datetime").cast(pl.Int64).alias("datetime_in_microsecs"),
)
)
shape: (6, 5)
┌────────────┬────────────┬───────┬──────────────┬───────────────────────┐
│ date ┆ strftime ┆ month ┆ date_in_days ┆ datetime_in_microsecs │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ date ┆ str ┆ i8 ┆ i32 ┆ i64 │
╞════════════╪════════════╪═══════╪══════════════╪═══════════════════════╡
│ 2025-01-01 ┆ 2025/01/01 ┆ 1 ┆ 20089 ┆ 1735689600000000 │
│ 2025-02-01 ┆ 2025/02/01 ┆ 2 ┆ 20120 ┆ 1735704000000000 │
│ 2025-03-01 ┆ 2025/03/01 ┆ 3 ┆ 20148 ┆ 1735718400000000 │
│ 2025-04-01 ┆ 2025/04/01 ┆ 4 ┆ 20179 ┆ 1735732800000000 │
│ 2025-05-01 ┆ 2025/05/01 ┆ 5 ┆ 20209 ┆ 1735747200000000 │
│ 2025-06-01 ┆ 2025/06/01 ┆ 6 ┆ 20240 ┆ 1735761600000000 │
└────────────┴────────────┴───────┴──────────────┴───────────────────────┘
pl.String
轉換為pl.Date
下面這段程式碼展示了:
pl.Date
型別。pl.Duration
型別。(
df.with_columns(
pl.col("date_str").str.to_date().alias("to_date"),
)
.with_columns(
pl.col("date").sub(pl.col("to_date")).alias("duration"),
)
.select(
pl.col("date_str", "to_date", "duration"),
pl.col("duration").dt.total_days().alias("duration_days"),
pl.col("duration").dt.total_hours().alias("duration_hours"),
)
)
shape: (6, 5)
┌────────────┬────────────┬──────────────┬───────────────┬────────────────┐
│ date_str ┆ to_date ┆ duration ┆ duration_days ┆ duration_hours │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ date ┆ duration[ms] ┆ i64 ┆ i64 │
╞════════════╪════════════╪══════════════╪═══════════════╪════════════════╡
│ 2025-07-05 ┆ 2025-07-05 ┆ -185d ┆ -185 ┆ -4440 │
│ 2025-08-05 ┆ 2025-08-05 ┆ -185d ┆ -185 ┆ -4440 │
│ 2025-09-10 ┆ 2025-09-10 ┆ -193d ┆ -193 ┆ -4632 │
│ 2025-10-10 ┆ 2025-10-10 ┆ -192d ┆ -192 ┆ -4608 │
│ 2025-11-20 ┆ 2025-11-20 ┆ -203d ┆ -203 ┆ -4872 │
│ 2025-12-20 ┆ 2025-12-20 ┆ -202d ┆ -202 ┆ -4848 │
└────────────┴────────────┴──────────────┴───────────────┴────────────────┘
pl.Date/DateTime
與pl.Time
為pl.Datetime
下面這段程式碼展示了:
pl.Expr.cast
將「"time"」列轉換為pl.Int64
型別,可以得到其距離午夜的秒數(單位為nanoseconds)。(
df.select(
"date",
"time",
pl.col("date")
.dt.combine(pl.col("time"))
.alias("combined_datetime"),
pl.col("time").cast(pl.Int64).alias("time_in_nanosecs"),
)
)
shape: (6, 4)
┌────────────┬──────────┬─────────────────────┬──────────────────┐
│ date ┆ time ┆ combined_datetime ┆ time_in_nanosecs │
│ --- ┆ --- ┆ --- ┆ --- │
│ date ┆ time ┆ datetime[μs] ┆ i64 │
╞════════════╪══════════╪═════════════════════╪══════════════════╡
│ 2025-01-01 ┆ 13:00:00 ┆ 2025-01-01 13:00:00 ┆ 46800000000000 │
│ 2025-02-01 ┆ 13:05:00 ┆ 2025-02-01 13:05:00 ┆ 47100000000000 │
│ 2025-03-01 ┆ 13:10:00 ┆ 2025-03-01 13:10:00 ┆ 47400000000000 │
│ 2025-04-01 ┆ 13:15:00 ┆ 2025-04-01 13:15:00 ┆ 47700000000000 │
│ 2025-05-01 ┆ 13:20:00 ┆ 2025-05-01 13:20:00 ┆ 48000000000000 │
│ 2025-06-01 ┆ 13:25:00 ┆ 2025-06-01 13:25:00 ┆ 48300000000000 │
└────────────┴──────────┴─────────────────────┴──────────────────┘
Python的zoneinfo.available_timezones()
可以列出Polars支援的時區:
import zoneinfo
print(zoneinfo.available_timezones())
下面這段程式碼展示了:
(
df.select(
pl.col("datetime_utc"),
pl.col("datetime_utc")
.dt.convert_time_zone("Asia/Taipei")
.alias("convert_tz_tpe"),
pl.col("datetime_utc")
.dt.replace_time_zone("Asia/Taipei")
.alias("replace_tz_tpe"),
)
)
shape: (6, 3)
┌─────────────────────────┬───────────────────────────┬
│ datetime_utc ┆ convert_tz_tpe ┆
│ --- ┆ --- ┆
│ datetime[μs, UTC] ┆ datetime[μs, Asia/Taipei] ┆
╞═════════════════════════╪═══════════════════════════╪
│ 2025-01-01 00:00:00 UTC ┆ 2025-01-01 08:00:00 CST ┆
│ 2025-02-01 00:00:00 UTC ┆ 2025-02-01 08:00:00 CST ┆
│ 2025-03-01 00:00:00 UTC ┆ 2025-03-01 08:00:00 CST ┆
│ 2025-04-01 00:00:00 UTC ┆ 2025-04-01 08:00:00 CST ┆
│ 2025-05-01 00:00:00 UTC ┆ 2025-05-01 08:00:00 CST ┆
│ 2025-06-01 00:00:00 UTC ┆ 2025-06-01 08:00:00 CST ┆
└─────────────────────────┴───────────────────────────┴
┬───────────────────────────┐
┆ replace_tz_tpe │
┆ --- │
┆ datetime[μs, Asia/Taipei] │
╪═══════════════════════════╡
┆ 2025-01-01 00:00:00 CST │
┆ 2025-02-01 00:00:00 CST │
┆ 2025-03-01 00:00:00 CST │
┆ 2025-04-01 00:00:00 CST │
┆ 2025-05-01 00:00:00 CST │
┆ 2025-06-01 00:00:00 CST │
┴───────────────────────────
請留意,Pl.Expr.dt.convert_time_zone()
是實際上對pl.Datetime
中的時區資訊進行轉換,而Pl.Expr.dt.replace_time_zone()
則是將pl.Datetime
中的時區資訊取代(也可以想成指定)為另一個時區。
依照這個邏輯,我們既然可以使用Pl.Expr.dt.replace_time_zone()
指定時區,那麼應該也可以將時區資訊自pl.Datetime
刪去。實際上,如果將None
傳入Pl.Expr.dt.replace_time_zone()
的確可以達成這樣的效果:
(
df.select(
pl.col("datetime_utc"),
pl.col("datetime_utc")
.dt.replace_time_zone(None)
.alias("no_tz"),
)
)
shape: (6, 2)
┌─────────────────────────┬─────────────────────┐
│ datetime_utc ┆ no_tz │
│ --- ┆ --- │
│ datetime[μs, UTC] ┆ datetime[μs] │
╞═════════════════════════╪═════════════════════╡
│ 2025-01-01 00:00:00 UTC ┆ 2025-01-01 00:00:00 │
│ 2025-02-01 00:00:00 UTC ┆ 2025-02-01 00:00:00 │
│ 2025-03-01 00:00:00 UTC ┆ 2025-03-01 00:00:00 │
│ 2025-04-01 00:00:00 UTC ┆ 2025-04-01 00:00:00 │
│ 2025-05-01 00:00:00 UTC ┆ 2025-05-01 00:00:00 │
│ 2025-06-01 00:00:00 UTC ┆ 2025-06-01 00:00:00 │
└─────────────────────────┴─────────────────────┘
pl.DataFrame.group_by_dynamic()
pl.DataFrame.group_by_dynamic()可以讓我們依據想要的時間間隔進行分組聚合。例如,我們可以將「"date"」列依照兩個月一次的頻率分組(註2),將「"time"」列中的pl.Time
收集為pl.List
:
df.group_by_dynamic("date", every="2mo").agg(pl.col("time"))
shape: (3, 2)
┌────────────┬──────────────────────┐
│ date ┆ time │
│ --- ┆ --- │
│ date ┆ list[time] │
╞════════════╪══════════════════════╡
│ 2025-01-01 ┆ [13:00:00, 13:05:00] │
│ 2025-03-01 ┆ [13:10:00, 13:15:00] │
│ 2025-05-01 ┆ [13:20:00, 13:25:00] │
└────────────┴──────────────────────┘
除了指定時間間隔的every=
參數外,另一個常用的period=
參數可以指定聚合時所使用的時間段。例如,我們可以將「"date"」列,依照兩個月一次的頻率分組,並以三個月做聚合計算,收集「"time"」列中的pl.Time
:
(
df.group_by_dynamic("date", every="2mo", period="3mo").agg(
pl.col("time")
)
)
shape: (3, 2)
┌────────────┬────────────────────────────────┐
│ date ┆ time │
│ --- ┆ --- │
│ date ┆ list[time] │
╞════════════╪════════════════════════════════╡
│ 2025-01-01 ┆ [13:00:00, 13:05:00, 13:10:00] │
│ 2025-03-01 ┆ [13:10:00, 13:15:00, 13:20:00] │
│ 2025-05-01 ┆ [13:20:00, 13:25:00] │
└────────────┴────────────────────────────────┘
事實上,在不指定period=
時,其值會與every=
相等,也就是分組與聚合使用一樣的時間間隔。
此外,df.group_by_dynamic()
有一個常被大家忽略的好用參數group_by=
,可以幫助我們在針對temporal
型別做分組時,同時對其它列或expr也進行分組。例如我們可以同時對「"date"」列及pl.col("date_str").str.slice(-2)
進行分組聚合:
(
df.group_by_dynamic(
"date", every="2mo", group_by=pl.col("date_str").str.slice(-2)
).agg(pl.col("time"))
)
shape: (3, 3)
┌──────────┬────────────┬──────────────────────┐
│ date_str ┆ date ┆ time │
│ --- ┆ --- ┆ --- │
│ str ┆ date ┆ list[time] │
╞══════════╪════════════╪══════════════════════╡
│ 05 ┆ 2025-01-01 ┆ [13:00:00, 13:05:00] │
│ 10 ┆ 2025-03-01 ┆ [13:10:00, 13:15:00] │
│ 20 ┆ 2025-05-01 ┆ [13:20:00, 13:25:00] │
└──────────┴────────────┴──────────────────────┘
pl.DataFrame.upsample()
pl.DataFrame.upsample()可以幫助我們提高時間間隔取樣頻率。例如,我們可以將「"date"」列由四小時間隔提高為兩小時:
with pl.Config(tbl_rows=20):
print(
df.select("datetime", "date_str").upsample("datetime", every="2h")
)
shape: (11, 2)
┌─────────────────────┬────────────┐
│ datetime ┆ date_str │
│ --- ┆ --- │
│ datetime[μs] ┆ str │
╞═════════════════════╪════════════╡
│ 2025-01-01 00:00:00 ┆ 2025-07-05 │
│ 2025-01-01 02:00:00 ┆ null │
│ 2025-01-01 04:00:00 ┆ 2025-08-05 │
│ 2025-01-01 06:00:00 ┆ null │
│ 2025-01-01 08:00:00 ┆ 2025-09-10 │
│ 2025-01-01 10:00:00 ┆ null │
│ 2025-01-01 12:00:00 ┆ 2025-10-10 │
│ 2025-01-01 14:00:00 ┆ null │
│ 2025-01-01 16:00:00 ┆ 2025-11-20 │
│ 2025-01-01 18:00:00 ┆ null │
│ 2025-01-01 20:00:00 ┆ 2025-12-20 │
└─────────────────────┴────────────┘
當然,Polars沒辦法無中生有,提高取樣頻率後的值預設為null
,需要由使用者填入適當的值,例如使用pl.Expr.fill_null(),以「"forward"」做為strategy=
參數,填補缺失值:
with pl.Config(tbl_rows=20):
print(
df.select("datetime", "date_str")
.upsample("datetime", every="2h")
.fill_null(strategy="forward")
)
shape: (11, 2)
┌─────────────────────┬────────────┐
│ datetime ┆ date_str │
│ --- ┆ --- │
│ datetime[μs] ┆ str │
╞═════════════════════╪════════════╡
│ 2025-01-01 00:00:00 ┆ 2025-07-05 │
│ 2025-01-01 02:00:00 ┆ 2025-07-05 │
│ 2025-01-01 04:00:00 ┆ 2025-08-05 │
│ 2025-01-01 06:00:00 ┆ 2025-08-05 │
│ 2025-01-01 08:00:00 ┆ 2025-09-10 │
│ 2025-01-01 10:00:00 ┆ 2025-09-10 │
│ 2025-01-01 12:00:00 ┆ 2025-10-10 │
│ 2025-01-01 14:00:00 ┆ 2025-10-10 │
│ 2025-01-01 16:00:00 ┆ 2025-11-20 │
│ 2025-01-01 18:00:00 ┆ 2025-11-20 │
│ 2025-01-01 20:00:00 ┆ 2025-12-20 │
└─────────────────────┴────────────┘
codepanda
相比於Polars,Pandas在時間相關型別提供了好用的offset alias。
舉例來說,假如我們想知道此次鐵人賽報名時間的每個星期三,分別是開始報名後的第幾天,可以使用W-WED
做為pd.DataFrame.resample()的規則。其中W
代表以每週為resample目標,而WED
則代表以星期三為分界。
idx = pd.date_range("2025-08-01", "2025-09-15")
df_pd = pd.DataFrame({"n": range(1, idx.size + 1)}).set_index(idx)
n
2025-08-01 1
2025-08-02 2
2025-08-03 3
2025-08-04 4
...
2025-09-12 43
2025-09-13 44
2025-09-14 45
2025-09-15 46
df_pd.resample("W-WED").max()
n
2025-08-06 6
2025-08-13 13
2025-08-20 20
2025-08-27 27
2025-09-03 34
2025-09-10 41
2025-09-17 46
註1:pl.Expr.dt.strftime()
會於底層呼叫pl.Expr.dt.to_string()。前者命名比較偏Python風格,而後者則比較偏Rust。
註2:關於時間間隔,Polars提供了一系列的寫法:
shape: (12, 2)
┌──────┬────────────────────┐
│ rule ┆ representation │
│ --- ┆ --- │
│ str ┆ str │
╞══════╪════════════════════╡
│ 1ns ┆ 1 nanosecond │
│ 1us ┆ 1 microsecond │
│ 1ms ┆ 1 millisecond │
│ 1s ┆ 1 second │
│ 1m ┆ 1 minute │
│ 1h ┆ 1 hour │
│ 1d ┆ 1 calendar day │
│ 1w ┆ 1 calendar week │
│ 1mo ┆ 1 calendar month │
│ 1q ┆ 1 calendar quarter │
│ 1y ┆ 1 calendar year │
│ 1i ┆ 1 index count │
└──────┴────────────────────┘